/*
 * Copyright [2009] [Marcin Rzeźnicki]

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
 */
package mr.go.yaxc;

import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;
import java.util.HashMap;
import java.util.Map;

import javax.xml.parsers.ParserConfigurationException;
import javax.xml.parsers.SAXParser;
import javax.xml.parsers.SAXParserFactory;

import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import org.xml.sax.SAXParseException;
import org.xml.sax.XMLReader;

/**
 * {@code XmlConfigurator} instances are entry points to XML parsing. Its
 * primary responsibility is to initiate and drive parsing process
 * 
 * @author Marcin Rzeźnicki
 * 
 */
public final class XmlConfigurator {

    private Map<String, Object> externalReferences =
            new HashMap<String, Object>();
    private SAXParserFactory factory;
    private boolean namespaceAware;
    private String namespaceUri = "";
    private SAXParser parser;
    private XMLReader reader;
    private boolean validating;
    private boolean xIncludeAware;

    private SAXParserFactory getFactory() {
        if (factory == null) {
            factory = SAXParserFactory.newInstance();
            factory.setNamespaceAware(namespaceAware);
            factory.setXIncludeAware(xIncludeAware);
            factory.setValidating(validating);
        }
        return factory;

    }

    /**
     *
     * @return XML namespace URI for yaxc nodes or empty string
     */
    public String getNamespaceUri() {
        return namespaceUri;
    }

    private SAXParser getParser()
            throws ParserConfigurationException,
                   SAXException {
        if (parser != null) {
            return parser;
        }
        parser = getFactory().newSAXParser();
        return parser;
    }

    private XMLReader getXMLReader()
            throws SAXException,
                   ParserConfigurationException {
        if (reader == null) {
            reader = getParser().getXMLReader();
        }
        final XmlConfigurationHandler handler = new XmlConfigurationHandler(
                namespaceUri);
        for (String externalRefId : externalReferences.keySet()) {
            handler.setReference(externalRefId,
                                 externalReferences.get(externalRefId));
        }
        reader.setContentHandler(handler);
        reader.setErrorHandler(handler);
        return reader;
    }

    /**
     *
     * @return true if parsing must support XML namespaces, false otherwise
     */
    public boolean isNamespaceAware() {
        return namespaceAware;
    }

    /**
     *
     * @return true if parsing must validate XML document, false otherwise
     */
    public boolean isValidating() {
        return validating;
    }

    /**
     *
     * @return true if parsing must process X-Include markup, false otherwise
     */
    public boolean isXIncludeAware() {
        return xIncludeAware;
    }

    private void nullify() {
        factory = null;
        parser = null;
        reader = null;
    }

    /**
     * Parses {@code url}
     *
     * @param url
     *            URL of XML file with configuration
     * @throws IOException
     *             when there is a problem with {@code file}
     * @throws ConfigurationError
     *             when there is a problem with carrying out configuration
     * @throws ParserConfigurationException
     *             when XML parser could not be created
     */
    public void parse(URL url)
            throws IOException, ConfigurationError,
                   ParserConfigurationException {
        if (url == null) {
            throw new IllegalArgumentException("URL to parse is null");
        }
        URLConnection connection = url.openConnection();
        connection.setUseCaches(false);
        InputStream stream = connection.getInputStream();
        InputSource source = new InputSource(stream);
        source.setSystemId(url.toExternalForm());
        try {
            getXMLReader().parse(source);
        } catch (SAXParseException e2) {
            throw new ConfigurationFileError(url.toString(),
                                             e2.getLineNumber(),
                                             e2.getColumnNumber(),
                                             e2);
        } catch (SAXException e3) {
            throw new ConfigurationFileError(url.toString(),
                                             e3);
        } finally {
            stream.close();
        }
    }

    /**
     * This enables actions to reference object {@code ref} during parsing. In
     * other words, {@code ref} is put in global context for all subsequent
     * parse calls
     *
     * @param id
     *            id of reference
     * @param ref
     *            value
     */
    public void setExternalReference(String id, Object ref) {
        externalReferences.put(id,
                               ref);
    }

    /**
     *
     * @param namespaceAware
     *            true if parsing must support XML namespaces, false otherwise
     */
    public void setNamespaceAware(boolean namespaceAware) {
        if (namespaceAware != this.namespaceAware) {
            nullify();
        }
        this.namespaceAware = namespaceAware;
    }

    /**
     *
     * @param namespaceUri
     *            namespace URI for yaxc nodes
     */
    public void setNamespaceUri(String namespaceUri) {
        this.namespaceUri = namespaceUri;
    }

    /**
     *
     * @param validating
     *            true if parsing must validate XML document, false otherwise
     */
    public void setValidating(boolean validating) {
        if (validating != this.validating) {
            nullify();
        }
        this.validating = validating;
    }

    /**
     *
     * @param includeAware
     *            true if parsing must process X-Include markup, false otherwise
     */
    public void setXIncludeAware(boolean includeAware) {
        if (includeAware != this.xIncludeAware) {
            nullify();
        }
        xIncludeAware = includeAware;
    }
}
